package com.me.waterphase.utils;

import java.util.ArrayList;

import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.CircleShape;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.Joint;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.joints.DistanceJoint;
import com.badlogic.gdx.physics.box2d.joints.DistanceJointDef;
import com.badlogic.gdx.physics.box2d.joints.RopeJoint;
import com.badlogic.gdx.physics.box2d.joints.RopeJointDef;
import com.badlogic.gdx.scenes.scene2d.ui.List;
import com.me.waterphase.WaterPlayer.FACING;
import com.me.waterphase.WaterWorld;
import com.me.waterphase.listener.EdgeListener;
import com.me.waterphase.listener.GroundListener;
import com.me.waterphase.listener.ListenerDispatcher;
import com.me.waterphase.listener.contact.EdgeContact;
import com.me.waterphase.listener.contact.FootContact;

public class PhysicManager {

	private static World myWorld;
	
	private static Box2DDebugRenderer debugger;
	private static OrthographicCamera camera;
	
	private static final float BOX_STEP=1/60f;
	private static final int  BOX_VELOCITY_ITERATIONS=8;
	private static final int BOX_POSITION_ITERATIONS=3;
	private static float accumulator;
	
	public static enum CONTACT_TYPE
	{
		USERDATA_ARTIFACT(1),
		USERDATA_PLAYER(2),
		USERDATA_GROUND_SENSOR(3),
		USERDATA_EDGE_SENSOR(4),
		USERDATA_ENERGY_SENSOR(5);
		
		private int type;
		
		private CONTACT_TYPE(int _type)
		{
			this.type = _type;
		}
		
		public int Type(){ return type; }
	}	
		
	/*
	 * Singleton for physic world.
	 */
	public static World getInstance(){
		
		if(myWorld == null){
			accumulator = 0.0f;
			myWorld = new World(new Vector2(0, Conversion.worldToBox(-2f)), true); 
			myWorld.setAutoClearForces(true);
			
			//Set callbacks.
			myWorld.setContactListener(new ListenerDispatcher());
			
			//Use to see the edges of bodies.
			debugger = new Box2DDebugRenderer(true, false, false, true);
			camera = new OrthographicCamera(2.75f, 2.75f);
			camera.position.set(0, 0, 0);
		}
		
		return myWorld;
	}
	
	/*
	 * Create a rectangle body with a width and an height.
	 */
	public static Body createRectBody(BodyType _type, float _posX, float _posY, float _angle, float _density, float _restitution, float _width, float _height, boolean _isSensor,  boolean _hasGroundSensor, boolean _isRotationFixed, boolean _hasEdgeSensor){
		
		//Definition of the body.
		BodyDef def = new BodyDef();
		def.type = _type;
		def.angle = _angle;
		def.position.set(Conversion.worldToBox(_posX+_width*0.5f), Conversion.worldToBox(_posY+_height*0.5f));
		
		Body tmpBody = PhysicManager.getInstance().createBody(def);
		tmpBody.setFixedRotation(_isRotationFixed);
		
		PhysicManager.makeRectFixture(0,0, _angle, _density, _restitution,_width,_height, _isSensor,  tmpBody);
		
		if(_hasGroundSensor){
			PhysicManager.makeGroundSensor(_width,_height, 0f, 0f, 0, tmpBody);
		}
		
		if(_hasEdgeSensor)
			PhysicManager.makeEdgeSensor(_width, _height, 0, 0, 0, tmpBody);
		
		return tmpBody;
	}
	
	/*
	 * Create a circle body with a radius.
	 */
	public static Body createCircleBody(BodyType _type, float _posX, float _posY, float _angle, float _density, float _restitution,  float _radius, boolean _hasGroundSensor, boolean _isRotationFixed){
		
		//Definition of the body.
		BodyDef def = new BodyDef();
		def.type = _type;
		def.angle = _angle;
		def.position.set(Conversion.worldToBox(_posX+_radius*0.5f), Conversion.worldToBox(_posY+_radius*0.5f));
		
		Body tmpBody = PhysicManager.getInstance().createBody(def);
		tmpBody.setFixedRotation(_isRotationFixed);
		
		PhysicManager.makeCircleFixture(0, 0, 0, _density, _restitution, _radius, tmpBody);
		
		if(_hasGroundSensor){
			if(_isRotationFixed)
				PhysicManager.makeGroundSensor(_radius*0.2f,_radius, 0f, 0f, 0, tmpBody);
			else
				PhysicManager.makeGroundSensor(_radius*0.5f, 0, 0, 0, tmpBody);
			
			tmpBody.setSleepingAllowed(false);
		}
		
		return tmpBody;
	}

	/*
	 * Create a polygon body with an array of Vector2. The vectors must have been reset to be relative to their own bound, not the world's ones. 
	 */
	public static Body createPolyBody(BodyType _type, float _posX, float _posY, float _angle, float _density, float _restitution,  Vector2[] _vectors, boolean _isRotationFixed){
		
		//Definition of the body.
		BodyDef def = new BodyDef();
		def.type = _type;
		def.angle = _angle;
		
		//Get the box around the shape.
		Vector2 widthAndHeight = PhysicManager.findVectorsWidthHeight(_vectors);
		
		def.position.set(Conversion.worldToBox(_posX+ widthAndHeight.x *0.5f), Conversion.worldToBox(_posY+ widthAndHeight.y *0.5f));
		
		Body tmpBody = PhysicManager.getInstance().createBody(def);
		tmpBody.setFixedRotation(_isRotationFixed);
		
		PhysicManager.makePolyFixture( _density, _restitution,_vectors, tmpBody);
		
		return tmpBody;
	}
	
	/*
	 * Create a body in capsule shape.
	 */
	public static Body createCapsuleBody(BodyType _type, float _posX, float _posY, float _angle, float _density, float _restitution, float _width, float _height, boolean _hasGroundSensor, boolean _isRotationFixed, boolean _hasEdgeSensor)
	{		
		float groundHeight = 0f;
		
		BodyDef def = new BodyDef();
		def.type = _type;
		def.angle = _angle;
		
		def.position.set(Conversion.worldToBox(_posX+_width*0.5f), Conversion.worldToBox(_posY+_height*0.5f));
		
		Body tmpBody = PhysicManager.getInstance().createBody(def);
		tmpBody.setFixedRotation(_isRotationFixed);		
				
		if(_height > _width)
		{
			//Create the 3 fixtures to make the capsule.
			PhysicManager.makeCircleFixture(0, (_height*0.5f-_width*0.5f),_angle,_density*0.33f, _restitution, _width, tmpBody);
			
			float polyHeight = _height-_width;
			
			ArrayList<Vector2> vectors = new ArrayList<Vector2>();
			
			//Creation of a polygon shape preventing the player to get stuck on edge.
			//Left side.
			vectors.add(new Vector2(-_width*0.004f, polyHeight*0.005f+_width*0.0025f));
			vectors.add(new Vector2(-_width*0.005f, polyHeight*0.005f));
			vectors.add(new Vector2(-_width*0.005f, -polyHeight*0.005f));
			vectors.add(new Vector2(-_width*0.004f, -polyHeight*0.005f+_width*0.0025f));
			
			//Right side.
			vectors.add(new Vector2(_width*0.004f, -polyHeight*0.005f+_width*0.0025f));
			vectors.add(new Vector2(_width*0.005f, -polyHeight*0.005f));
			vectors.add(new Vector2(_width*0.005f, polyHeight*0.005f));
			vectors.add(new Vector2(_width*0.004f, polyHeight*0.005f+_width*0.0025f));
			
			PhysicManager.makePolyFixture(_density*0.33f, _restitution, vectors.toArray(new Vector2[0]), tmpBody);
			PhysicManager.makeCircleFixture(0, (-(_height*0.5f)+(_width*0.5f)),_angle,_density*0.33f, _restitution, _width, tmpBody);
			
			groundHeight = _height;
		}
		else
		{
			groundHeight = _width;
			
			PhysicManager.makeCircleFixture(0, 0,_angle,_density, _restitution, _width, tmpBody);
		}
			
		//Place a ground sensor below the body.
		if(_hasGroundSensor)
		{
			PhysicManager.makeGroundSensor(_width*0.5f,groundHeight, 0f, 0f, 0, tmpBody);
			tmpBody.setSleepingAllowed(false);
		}
		
		if(_hasEdgeSensor)
			PhysicManager.makeEdgeSensor(_width, _height, 0, 0, 0, tmpBody);
		
		return tmpBody;
	}
	
	
	/*
	 * Find the biggest X and the biggest Y to know the width and the height of the box surrounding the polygonshape.
	 */
	public static Vector2 findVectorsWidthHeight(Vector2[] _vectors){
		
		float maxX = 0;
		float maxY = 0;
		
		for(int i = 0; i < _vectors.length; i++){
			if(maxX < _vectors[i].x)
				maxX = _vectors[i].x;
			
			if(maxY < _vectors[i].y)
				maxY = _vectors[i].y;
		}
		
		return new Vector2(maxX, maxY);
	}

	/*
	 * Create a rectangle definition for the body.
	 */
	public static void makeRectFixture(float _posX, float _posY, float _angle, float _density, float _restitution, float _width, float _height, boolean _isSensor, Body _body){
		
		PolygonShape bodyShape = new PolygonShape();
		
		float w = Conversion.worldToBox(_width*0.5f);
		float h = Conversion.worldToBox(_height*0.5f);
		
		float tmpX = Conversion.worldToBox(_posX);
		float tmpY = Conversion.worldToBox(_posY);
		
		bodyShape.setAsBox(w, h, new Vector2(tmpX, tmpY), _angle);
		
		FixtureDef fixDef = new FixtureDef();
		fixDef.density = _density;
		fixDef.restitution = _restitution;
		fixDef.shape = bodyShape;
		fixDef.isSensor = _isSensor;
		
		//Set the user's data to indicate that this fixture isn't a sensor.
		_body.createFixture(fixDef);		
		bodyShape.dispose();
		
	}
	
	/*
	 * Create a ground sensor for the body passed in parameter (polygon).
	 */
	public static void makeGroundSensor(float _width, float _height, float _angle, float _density, float _restitution, Body _body){
		PolygonShape bodyShape = new PolygonShape();
		
		//Hard-coded arbitrary values to have the sensor below the body.
		float w = Conversion.worldToBox(_width*0.5f);
		float h = 0.02f;
		
		bodyShape.setAsBox(w, h, new Vector2(0, -Conversion.worldToBox(_height*0.5f)), 0);
		
		FixtureDef fixDef = new FixtureDef();
		fixDef.density = _density;
		fixDef.restitution = _restitution;
		fixDef.shape = bodyShape;
		
		//The ground sensor is a fixture, and not a sensor, due to Box2d not resolving normal for sensor.
		//fixDef.isSensor = true;

		//Indicate that this fixture is a ground sensor.
		Fixture created = _body.createFixture(fixDef);
		created.setUserData(new FootContact());

		bodyShape.dispose();
	}
	
	/*
	 * Create a ground sensor for the body passed in parameter (circle).
	 */
	public static void makeGroundSensor(float _radius, float _angle, float _density, float _restitution, Body _body){
		CircleShape bodyShape = new CircleShape();
		
		//Hard-coded arbitrary values to have the sensor below the body.
		float w = Conversion.worldToBox(_radius + 1f);
		bodyShape.setRadius(w);
		
		FixtureDef fixDef = new FixtureDef();
		fixDef.density = _density;
		fixDef.restitution = _restitution;
		fixDef.shape = bodyShape;
		
		//Indicate that this fixture is a ground sensor.
		Fixture created = _body.createFixture(fixDef);
		created.setUserData(new FootContact());

		bodyShape.dispose();
	}
	
	/*
	 * Function creating a sensor at the top corner of the body to acknowledge if the body is near an edge.
	 */
	public static void makeEdgeSensor(float _width, float _height, float _angle, float _density, float _restitution, Body _body)
	{
		float tmpWidth = Conversion.worldToBox(_width);
		float tmpHeight = Conversion.worldToBox(_height);
		
		//Hard-coded arbitrary values to have the sensor at the corner of the body.
		float w = 0.02f;
		float h = 0.01f;
		
		float tmpX = tmpWidth*0.5f+w;
		float tmpY = tmpHeight*0.5f+h;
		
		//Creation of the fixture definition.
		FixtureDef partDef = new FixtureDef();
		partDef.density = _density;
		partDef.restitution = _restitution;
		partDef.isSensor = true;
		
		//Creation of the shape.
		PolygonShape upperPart = new PolygonShape();		
		upperPart.setAsBox(w, h, new Vector2(tmpX, tmpY), 0);
		partDef.shape = upperPart;		

		//Indicate that this fixture is an edge sensor.
		Fixture upperCreatedRight = _body.createFixture(partDef);
		upperCreatedRight.setUserData(new EdgeContact(0, true, FACING.RIGHT));
		
		//Set the left upper sensor.
		upperPart.setAsBox(w, h, new Vector2(-(tmpX), tmpY), 0);
		partDef.shape = upperPart;
		
		Fixture upperCreatedLeft = _body.createFixture(partDef);
		upperCreatedLeft.setUserData(new EdgeContact(0, true, FACING.LEFT));

		//Hard-coded arbitrary values to have the sensor below the body.
		h = tmpHeight*0.1f;
		tmpY = tmpHeight*0.5f-h;
		
		//Creation of the lower part of the sensor (right).
		PolygonShape lowerPart = new PolygonShape();
		lowerPart.setAsBox(w, h, new Vector2(tmpX, tmpY), 0);
		partDef.shape = lowerPart;

		//Indicate that this fixture is an edge sensor.
		Fixture lowerCreatedRight = _body.createFixture(partDef);
		lowerCreatedRight.setUserData(new EdgeContact(0, false, FACING.RIGHT));
		
		//Creation of the lower part of the sensor (left).
		lowerPart.setAsBox(w, h, new Vector2(-(tmpX), tmpY), 0);
		partDef.shape = lowerPart;

		//Indicate that this fixture is an edge sensor.
		Fixture lowerCreatedLeft = _body.createFixture(partDef);
		lowerCreatedLeft.setUserData(new EdgeContact(0, false, FACING.LEFT));
		
		upperPart.dispose();
		lowerPart.dispose();
	}
	
	/*
	 * Create a circle definition for the body.
	 */
	public static void makeCircleFixture(float _posX, float _posY, float _angle, float _density, float _restitution, float _radius, Body _body){
		
		float radius = Conversion.worldToBox(_radius*0.5f);
		
		float tmpX = Conversion.worldToBox(_posX);
		float tmpY = Conversion.worldToBox(_posY);
		
		FixtureDef fixDef = new FixtureDef();
		
		fixDef.density = _density;
		fixDef.restitution = _restitution;
		
		//Set the center of the shape.
		fixDef.shape = new CircleShape();
		((CircleShape)fixDef.shape).setPosition(new Vector2(tmpX, tmpY));
		
		fixDef.shape.setRadius(radius);
		
		//Set the user's data to indicate that this fixture isn't a sensor.
		_body.createFixture(fixDef);
		fixDef.shape.dispose();
	}
	
	/*
	 * Create a polygon fixture for the body. BEWARE! This is the only function that needs points already converted in Box2d dimension.
	 * A polygon shape accepts 8 vertices max. Do not forget the counter-clockwise rule. 
	 */
	public static void makePolyFixture(float _density, float _restitution, Vector2[] _vectors,  Body _body)
	{
		FixtureDef fixDef = new FixtureDef();
		
		fixDef.density = _density;
		fixDef.restitution = _restitution;
		
		PolygonShape tmpShape = new PolygonShape();
		tmpShape.set(_vectors);
		fixDef.shape = tmpShape;
		
		//Set the user's data to indicate that this fixture isn't a sensor.
		 _body.createFixture(fixDef);		
		fixDef.shape.dispose();
	}
		
	/*
	 * Create a rope between two bodies.
	 */
	public static void createRopeJoint(float _maxLength, Body _bodyA, Body _bodyB, Vector2 _anchorA, Vector2 _anchorB)
	{
		RopeJointDef ropeDef = new RopeJointDef();
		
		ropeDef.bodyA = _bodyA;
		ropeDef.bodyB = _bodyB;
		ropeDef.localAnchorA.set(_anchorA);
		ropeDef.localAnchorB.set(_anchorB);
		ropeDef.maxLength = Conversion.worldToBox(_maxLength);
		ropeDef.collideConnected = true;
		
		PhysicManager.getInstance().createJoint(ropeDef);
	}
	
	/*
	 * Create a distance joint between two bodies.
	 */
	public static void createDistanceJoint(float _maxLength, Body _bodyA, Body _bodyB, Vector2 _anchorA, Vector2 _anchorB)
	{
		DistanceJointDef jointDef = new DistanceJointDef();
		
		jointDef.bodyA = _bodyA;
		jointDef.bodyB = _bodyB;
		jointDef.collideConnected = true;
		jointDef.length = Conversion.worldToBox(_maxLength);
		jointDef.localAnchorA.set(_anchorA);
		jointDef.localAnchorB.set(_anchorB);
		
		PhysicManager.getInstance().createJoint(jointDef);
		
	}
	
	/*
	 * Make the world steps until its delta time is reduced to 0. Step is of 60fps.
	 */
	public static void Update(float _deltaTime){
		accumulator += _deltaTime;
		int i = 0;
		
		while ((accumulator > BOX_STEP)&&(i < 40)){
			PhysicManager.getInstance().step(BOX_STEP, BOX_VELOCITY_ITERATIONS, BOX_POSITION_ITERATIONS);
			accumulator -= BOX_STEP;
			i++;
		}
		
		if(i == 40)
			_deltaTime = 0;
	}
	
	/*
	 * Render debug camera.
	 */
	public static void renderDebug(Vector2 _focusPoint)
	{
		//Debug camera to see bodies.
		camera.position.x = Conversion.worldToBox(_focusPoint.x);
		camera.position.y = Conversion.worldToBox(_focusPoint.y);
		
		camera.update();
		debugger.render(PhysicManager.getInstance(), camera.combined);
	}
	
	
}
